import React, {Fragment, ReactNode, useRef} from 'react'
import {getContextByName, getCurrentDesignInstance, inject, provide, TopDesign, useProvideManager} from "./composition/context";
import {useHookManager, useOnce, useRender} from "./composition/hook";
import "./composition/installJSXModelPlugin";
import type {ComponentPropsOptions, ExtractPropTypes, PropType} from './composition/prop.type'
import {useEvent} from "./composition/emit";
import {shallowReactive} from './composition/reactivity';
import {createUseBeforeRenderManager, onBeforeMount, onBeforeRender, onBeforeUnmount, onBeforeUpdate, onMounted, onUnmounted, onUpdated, useHooksOnDesign} from "./composition/lifecycle";
import {useSetupSlots} from "./composition/slot";
import {useSetupScopeSlot} from "./composition/scope-slot";
import useClasses from "./use/useClasses";
import useModel from "./use/useModel";
import {useMounted} from './use/useMounted';
import {useStyles} from './use/useStyles';
import {useNumber} from './use/useNumber';
import {checkConflictProps, mergeProps, useSetupProps} from "./composition/prop";
import {isFragment} from "react-is";
import {useRefs} from "./use/useRefs";
import {useRefList} from "./use/useRefList";
import {classnames} from "./utils/classnames";
import {createCounter} from "./utils/createCounter";
import {defer} from "./utils/defer";
import {nextIndex} from "./utils/nextIndex";
import {toArray} from "./utils/toArray";
import {fixInputCursor} from './utils/fixInputCursor';
import {createEventListener} from './utils/createEventListener';
import {isDom} from './utils/isDom';
import type {ComponentEvent, EmitToProp, ObjectEmitOptions} from './composition/emit.type';
import type {SlotProps} from "./composition/slot.type";
import type {ScopeSlotProps} from "./composition/scope-slot.type";
import type {TopDesignContext} from './composition/context.type';
import type {SimpleFunction} from './composition/event.type';
import type {MultipleClass, RenderNode, SingleClass, StyleProperties} from './type';
import {useImperativeRefer} from "./use/useImperativeRefer";
import {createPlainEvent} from "./composition/event";
import {processSetupRender} from "./composition/processSetupRender";
import {Portal, Teleport} from './composition/Portal';
import {createHooks} from "./utils/createHooks";
import {createSyncHooks} from "./utils/createSyncHooks";
import {_watch} from "./effect/_watch";
import {_watchEffect} from "./effect/_watchEffect";
import {delay} from "./utils/delay";

export interface InjectValue<Refer> {
    (): Refer,

    <DefaultValue>(defaultValue?: DefaultValue): Refer | DefaultValue
}

export interface UseType<Refer, Props> {
    ref: () => { current: Refer | null },
    inject: InjectValue<Refer>
    class: Refer,
    props: Props,
}

function designComponent<_,
    Expose extends object,
    SetupProps extends Readonly<ExtractPropTypes<PropsOptions> & { children: any }>,
    FcProps extends ExtractPropTypes<PropsOptions, false>,
    SlotKeys extends string = '',
    PropsOptions extends Readonly<ComponentPropsOptions> = {},
    EmitOptions extends ObjectEmitOptions = {},
    ScopeSlots extends { [k: string]: (scope: any) => void } = {},
    InheritPropsType extends (((props: any) => any) | (new() => any) | Record<string, any>) = {},
    Refer = unknown,
    FcPropsType = FcProps
        & (FcProps extends { modelValue?: any } ? { 'v-model'?: FcProps['modelValue'] } : {}) &
        {
            className?: string,
            style?: StyleProperties,
            ref?: null | undefined | { current: Refer | null | undefined } | ((refer: Refer | null | undefined) => void),
            onRef?: null | undefined | { current: Refer | null | undefined } | ((refer: Refer | null | undefined) => void),
        } &
        {
            onBeforeMount?: () => void,
            onMounted?: () => void,
            onBeforeUpdate?: () => void,
            onUpdated?: () => void,
            onBeforeUnmount?: () => void,
            onUnmounted?: () => void,
        } &
        EmitToProp<EmitOptions> &
        SlotProps<SlotKeys, ScopeSlots> &
        ScopeSlotProps<SlotKeys, ScopeSlots> &
        { children?: (SlotProps<SlotKeys, ScopeSlots> & ScopeSlotProps<SlotKeys, ScopeSlots>) | never[] | ('default' extends SlotKeys ? (ReactNode | (() => ReactNode)) : ('default' extends keyof ScopeSlots ? ((scope: Parameters<ScopeSlots['default']>[0]) => ReactNode) : never)) },
    __ = {}, >(
    {
        name,
        provideRefer,
        inheritAttrs,
        props,
        emits,
        setup,
        expose,
        slots,
        scopeSlots,
    }: {
        name?: string,
        provideRefer?: boolean,
        inheritAttrs?: boolean,
        inheritPropsType?: InheritPropsType,
        props?: PropsOptions,
        emits?: EmitOptions,
        expose?: Expose,
        slots?: SlotKeys[],
        scopeSlots?: ScopeSlots,
        setup?: (data: {
            attrs: Record<string, any>,
            props: SetupProps,
            event: ComponentEvent<EmitOptions>,
            slots: { [k in SlotKeys]: ((defaultReactNode?: ReactNode) => ReactNode) & { isExist: () => boolean } },
            scopeSlots: { [k in keyof ScopeSlots]: ((scope: Parameters<ScopeSlots[k]>[0], defaultNode?: ReactNode) => ReactNode) & { isExist: () => boolean } },
        }) => ({ refer?: Refer, render: () => any } | (() => any)),
    }
) {

    const fc: React.FC<FcPropsType & (
        InheritPropsType extends ((props: infer InheritProps) => any) ?
            Omit<InheritProps, keyof FcPropsType> :
            InheritPropsType extends (new() => infer R) ? Omit<React.DetailedHTMLProps<React.HTMLAttributes<R>, R>, keyof FcPropsType> :
                Omit<InheritPropsType, keyof FcPropsType>
        )> = React.forwardRef((fcProps: any, ref) => {

        const {fcPropsPersist} = (() => {
            const fcPropsPersist = useRef(fcProps)
            fcPropsPersist.current = {...fcProps}
            fcPropsPersist.current.ref = ref
            return {fcPropsPersist}
        })();

        const {setupProps, setupAttrs} = useSetupProps({
            fcProps,
            emitOptions: emits,
            propOptions: props,
            slotOptions: slots,
            scopeSlotOptions: scopeSlots,
        })
        const {setupEvent} = useEvent(fcProps, emits)
        const {setupSlots} = useSetupSlots({fcPropsPersist, slots})
        const {setupScopeSlot} = useSetupScopeSlot(fcProps, scopeSlots)
        const {hookManager} = useHookManager()
        const {provideManager} = useProvideManager()
        const {useBeforeRenderManager} = createUseBeforeRenderManager()
        const ctx = useRef({} as TopDesignContext)

        const {setupRender: render} = useOnce(() => {
            /*检查有冲突的props*/
            checkConflictProps({propsOptions: props, emitOptions: emits, slotOptions: slots, scopeSlotOptions: scopeSlots,})
            const setupOption = {attrs: setupAttrs, props: setupProps, event: setupEvent, slots: setupSlots, scopeSlots: setupScopeSlot,}
            /*组件上下文*/
            ctx.current = TopDesign.ctx = {
                ...setupOption,
                isDestroyed: false as boolean,
                useProvide: provideManager.useProvide,
                useHook: hookManager.useHook,
                useBeforeRender: useBeforeRenderManager.useBeforeRender,
                effects: [],
                /*proxy对象的引用不能改变*/
                proxy: {},
            }
            /*没有setup*/
            if (!setup) {return ({setupRender: () => null})}
            const setupData = setup(setupOption)
            const setupRender = (() => {
                const _render = typeof setupData === "function" ? setupData : setupData.render
                let fn = () => {
                    if (ctx.current.isDestroyed) {return null}
                    return _render()
                }
                const setupRenderProcessor = processSetupRender.get()
                if (!!setupRenderProcessor) {
                    fn = setupRenderProcessor(fn)
                }
                return fn
            })();
            Object.assign(ctx.current.proxy, typeof setupData === "function" ? {} : setupData.refer)

            /*组件销毁的时候，标记为已经销毁*/
            onBeforeUnmount(() => {
                ctx.current.isDestroyed = true
                ctx.current.effects.forEach(fn => fn());
            })

            /*expose lifecycle hook*/
            !!fcPropsPersist.current.onBeforeMount && onBeforeMount(fcPropsPersist.current.onBeforeMount);
            !!fcPropsPersist.current.onMounted && onMounted(fcPropsPersist.current.onMounted);
            !!fcPropsPersist.current.onBeforeUpdate && onBeforeUpdate(fcPropsPersist.current.onBeforeUpdate);
            !!fcPropsPersist.current.onUpdated && onUpdated(fcPropsPersist.current.onUpdated);
            !!fcPropsPersist.current.onBeforeUnmount && onBeforeUnmount(fcPropsPersist.current.onBeforeUnmount);
            !!fcPropsPersist.current.onUnmounted && onUnmounted(fcPropsPersist.current.onUnmounted);

            /*provide refer*/
            if (provideRefer) {
                if (!name) {
                    console.error('designComponent: component name is necessary when provideRefer is true!')
                } else {
                    if (typeof setupData === "function") {
                        console.error('designComponent: setup must return object {refer,render} when provideRefer is true!')
                    } else {
                        provide(`@@${name}`, ctx.current.proxy)
                    }
                }
            }

            /*处理ref以及onRef*/
            useHooksOnDesign(() => {
                useImperativeRefer(fcPropsPersist.current.ref, ctx.current.proxy)
                useImperativeRefer(fcPropsPersist.current.onRef, ctx.current.proxy)
            })

            TopDesign.ctx = null
            return {setupRender}
        })
        hookManager.execHook()

        /*依赖变化时强制更新render computed*/
        let computedRender = useRender({
            render,
            ctx: ctx.current,
            onBeforeRender: useBeforeRenderManager.execBeforeRender,
            memoDependencies: (() => {
                let keys = [] as string[]
                if (!!slots) {keys.push(...slots)}
                if (keys.length > 0) {keys.push('children')}
                return keys.map(key => fcProps[key])
            })(),
        });
        /*---------------------------------------inheritAttrs 继承属性-------------------------------------------*/
        ;(() => {
            /*不继承额外的属性，或者没有需要继承到根节点的属性*/
            const passPropNames = Object.keys(setupAttrs)
            if (inheritAttrs === false || passPropNames.length === 0) {return;}
            if (typeof computedRender !== "object" || Array.isArray(computedRender) || isFragment(computedRender)) {
                /*根节点是fragment或者数组或者text node，提出警告*/
                console.warn(`Extraneous non-props attributes (${passPropNames.join(',')}) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. `)
            } else {
                computedRender = mergeProps({child: computedRender, attrs: setupAttrs})
            }
        })();

        return provideManager.wrap(computedRender)
    }) as any

    return Object.assign(fc, {
        options: {
            name,
            props,
            emits,
            slots,
            scopeSlots,
        },
        use: {
            inject: (defaultValue?: any) => inject(`@@${name}`, defaultValue) as Refer,
            ref: () => useReference<Refer>(),
            class: Object as any as Refer,
            props: Object as any as SetupProps,
        } as UseType<Refer, SetupProps>
    }, expose)
}

function useReference<T = any>(defaultValue?: T) {
    return shallowReactive({
        current: defaultValue || null,
    }) as { current: null | T }
}

const designClassComponent: typeof designComponent = (option) => {
    const SFC = designComponent(option)
    const {...expose} = SFC
    const Cls = class extends React.Component<any, any> {
        render() {
            return <SFC {...this.props as any} ref={refer => !!refer && Object.assign(this, refer)}/>
        }
    }
    return Object.assign(Cls, expose) as any
}

const designPage = (setup: (props: any) => any) => designComponent({
    inheritAttrs: false,
    setup: ({attrs}) => setup(attrs)
})

export * from './composition/reactivity'

export type {
    ExtractPropTypes, PropType, TopDesignContext, ObjectEmitOptions, ComponentEvent, EmitToProp, SingleClass, MultipleClass,
    SimpleFunction, StyleProperties, ComponentPropsOptions, RenderNode,
}

export const watch = _watch

export const watchEffect = _watchEffect

export const nextTick = delay;

export {
    designPage, designComponent, designClassComponent,

    provide, inject, getCurrentDesignInstance,

    onMounted, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onUpdated, onUnmounted, onBeforeRender, useHooksOnDesign,

    useClasses, useModel, useMounted, useNumber, useStyles, useRefs, useRefList, useReference,

    useImperativeRefer,

    classnames, createCounter, defer, nextIndex, toArray, fixInputCursor, createEventListener, mergeProps, isDom, getContextByName, createPlainEvent, processSetupRender,
    Fragment, Portal, Teleport, createHooks, createSyncHooks,
}
